using System;
using System.Collections;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Reflection;
using System.Runtime.Remoting.Messaging;
using System.Text;
using System.Threading;
using System.Web;
using System.Windows.Forms;
using System.Globalization;
using System.Collections.Generic;
using System.Runtime.InteropServices;

namespace FreeFlow.WinFormsControls.HelperClasses
{
  /// <summary>Where exceptions will be logged to</summary>
  public enum ExceptionLogType
  {
    /// <summary>Log to text file</summary>
    TextFile,
    /// <summary>Log to event log</summary>
    EventLog,
    /// <summary>Log to a website</summary>
    WebSite
  }

  // use to get memory available
  [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
  internal class MEMORYSTATUSEX
  {
    public uint dwLength;
    public uint dwMemoryLoad;
    public ulong ullTotalPhys;
    public ulong ullAvailPhys;
    public ulong ullTotalPageFile;
    public ulong ullAvailPageFile;
    public ulong ullTotalVirtual;
    public ulong ullAvailVirtual;
    public ulong ullAvailExtendedVirtual;

    public MEMORYSTATUSEX()
    {
      this.dwLength = (uint)Marshal.SizeOf(typeof(MEMORYSTATUSEX));
    }
  }

  internal static class NativeMethods
  {
    [return: MarshalAs(UnmanagedType.Bool)]
    [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    internal static extern bool GlobalMemoryStatusEx([In, Out] MEMORYSTATUSEX lpBuffer);
  }

  /// <summary>
  /// Class used to log unhandled exceptions.
  /// </summary>
  public class ExceptionLogger
  {
    /// <summary>
    /// Initializes a new instance of the <see cref="ExceptionLogger"/> class. 
    /// Typically this will be one of the first things to be created in the application's Main() method. 
    /// </summary>
    public ExceptionLogger()
    {
      Application.ThreadException += 
        new System.Threading.ThreadExceptionEventHandler(OnThreadException);
    }

    /// <summary>
    /// Logs an exception asynchronously.
    /// </summary>
    /// <param name="ex">The exception.</param>
    /// <param name="logType">The type of logging required.</param>
    public static void LogException(Exception ex, ExceptionLogType logType)
    {
      ExceptionLogger logger = new ExceptionLogger();
      logger.LogType = logType;
      logger.BeginLogException(ex);
    }

    delegate void LogExceptionDelegate(string emailAddress, Exception e);

    // Event handler that will be called when an unhandled
    // exception is caught
    private void OnThreadException(object sender, ThreadExceptionEventArgs e)
    {
      BeginLogException(e.Exception);
    }

    private void BeginLogException(Exception ex)
    {
      string emailAddress = "";
      switch (logType)
      {
        case ExceptionLogType.WebSite :
          using (ExceptionForm form = new ExceptionForm(ex))
          {
            if (form.ShowDialog() == DialogResult.No)
              return;
            emailAddress = form.EmailAddress;
          }
          break;
        case ExceptionLogType.EventLog :
          MessageBox.Show("An unexpected error occurred - " + ex.Message + 
            ". It will be logged to the event log", "Error");
          break;
        case ExceptionLogType.TextFile :
          MessageBox.Show("An unexpected error occurred - " + ex.Message + 
            ". It will be logged to the text file 'BugReport.txt'", "Error");
          break;
        default :
          Debug.Assert(false, "Unrecognised log type - " + logType.ToString());
          break;
      }
      

      // Log the exception
      LogExceptionDelegate logDelegate = new LogExceptionDelegate(LogException); 
      logDelegate.BeginInvoke(emailAddress, ex, new AsyncCallback(LogCallBack), null);
    }

    private void LogCallBack(IAsyncResult result)
    {
      LogExceptionDelegate logDelegate = (LogExceptionDelegate)((AsyncResult)result).AsyncDelegate;
      logDelegate.EndInvoke(result);
    }

    private ExceptionLogType logType;
    /// <summary>
    /// Gets or sets the type of logging that will occur.
    /// </summary>
    public ExceptionLogType LogType
    {
      get { return logType; }
      set { logType = value; }
    }

    private static TimeSpan GetSystemUpTime()
    {
      PerformanceCounter upTime = new PerformanceCounter("System", "System Up Time");
      upTime.NextValue();
      return TimeSpan.FromSeconds(upTime.NextValue());
    }

    private static string GetExceptionTypeStack(Exception e)
    {
      if (e.InnerException != null)
      {
        StringBuilder message = new StringBuilder();
        message.AppendLine(GetExceptionTypeStack(e.InnerException));
        return (message.ToString());
      }
      else
      {
        return ("   " + e.GetType().ToString());
      }
    }

    private static string GetExceptionMessageStack(Exception e)
    {
      if (e.InnerException != null)
      {
        StringBuilder message = new StringBuilder();
        message.AppendLine(GetExceptionMessageStack(e.InnerException));
        return (message.ToString());
      }
      else
      {
        return ("   " + e.Message);
      }
    }

    private static string GetExceptionCallStack(Exception e)
    {
      if (e.InnerException != null)
      {
        StringBuilder message = new StringBuilder();
        message.AppendLine(GetExceptionCallStack(e.InnerException));
        message.AppendLine("--- Next Call Stack:");
        return (message.ToString());
      }
      else
      {
        return (e.StackTrace);
      }
    }

    /// <summary>
    /// Generates an exception log for the specified exception.
    /// </summary>
    /// <param name="exception">The exception.</param>
    public static string GenerateExceptionLog(Exception exception)
    {
      StringBuilder error = new StringBuilder();

      error.AppendLine("Application:       " + Application.ProductName);
      error.AppendLine("Version:           " + Application.ProductVersion);
      error.AppendLine("Date:              " + DateTime.Now.ToString("dd/MM/yyyy HH:mm:ss"));
      error.AppendLine("Computer name:     " + SystemInformation.ComputerName);
      error.AppendLine("User name:         " + SystemInformation.UserName);
      error.AppendLine("OS:                " + Environment.OSVersion.ToString());
      error.AppendLine("Culture:           " + CultureInfo.CurrentCulture.Name);
      error.AppendLine("Resolution:        " + SystemInformation.PrimaryMonitorSize.ToString());
      error.AppendLine("System up time:    " + GetSystemUpTime());
      error.AppendLine("App up time:       " +
        (DateTime.Now - Process.GetCurrentProcess().StartTime).ToString());

      MEMORYSTATUSEX memStatus = new MEMORYSTATUSEX();
      if (NativeMethods.GlobalMemoryStatusEx(memStatus))
      {
        error.AppendLine("Total memory:      " + memStatus.ullTotalPhys / (1024 * 1024) + "Mb");
        error.AppendLine("Available memory:  " + memStatus.ullAvailPhys / (1024 * 1024) + "Mb");
      }

      error.AppendLine("");

      error.AppendLine("Exception classes:   ");
      error.AppendLine(GetExceptionTypeStack(exception));
      error.AppendLine("");
      error.AppendLine("Exception messages: ");
      error.AppendLine(GetExceptionMessageStack(exception));

      error.AppendLine("");
      error.AppendLine("Stack Traces:");
      error.AppendLine(GetExceptionCallStack(exception));
      error.AppendLine("");
      error.AppendLine("Loaded Modules:");
      Process thisProcess = Process.GetCurrentProcess();
      foreach (ProcessModule module in thisProcess.Modules)
      {
        error.AppendLine(module.FileName + " " + module.FileVersionInfo.FileVersion);
      }

      return error.ToString();
    }

    /// <summary>
    /// writes exception details to a log file
    /// </summary>
    private void LogException(string emailAddress, Exception exception) 
    {
      string log = GenerateExceptionLog(exception);
      if (!string.IsNullOrEmpty(emailAddress))
        log = emailAddress + "\n" + log;
      
      switch (logType)
      {
        case ExceptionLogType.TextFile : 
          LogToFile(log.ToString());
          break;
        case ExceptionLogType.EventLog :
          LogToEventLog(log.ToString());
          break;
        case ExceptionLogType.WebSite : 
          LogToWebsite(log.ToString());
          break;
        default : 
          Debug.Assert(false);
          break;
      }
    }

    private static void LogToEventLog(string error)
    {
      EventLog log = new EventLog("Application");
      log.Source = Assembly.GetExecutingAssembly().ToString();
      log.WriteEntry(error, EventLogEntryType.Error);
    }

    private void LogToFile(string error)
    {
      string filename = Path.GetDirectoryName(Application.ExecutablePath);
      filename += "\\BugReport.txt";

      List<string> data = new List<string>();

      lock (this)
      {
        if (File.Exists(filename))
        {
          using (StreamReader reader = new StreamReader(filename))
          {
            string line = null;
            do 
            {
              line = reader.ReadLine();
              data.Add(line);
            } 
            while (line != null);
          }
        }

        // truncate the file if it's too long
        int writeStart = 0;
        if (data.Count > 500)
          writeStart = data.Count - 500;

        using (StreamWriter stream = new StreamWriter(filename, false))
        {
          for (int i = writeStart; i < data.Count; i++)
          {
            stream.WriteLine(data[i]);
          }
    
          stream.Write(error);
        }
      }
    }

    private string url;
    /// <summary>
    /// Gets or sets the URL that will be used when posting an error to a website.
    /// </summary>
    public string Url
    {
      get
      {
        return url;
      }
      set
      {
        url = value;
      }
    }

    private string queryString;
    /// <summary>
    /// Gets or sets the format of the query string that will be used when posting an error to a website. 
    /// e.g error={0}
    /// </summary>
    public string QueryString
    {
      get
      {
        return queryString;
      }
      set
      {
        queryString = value;
      }
    }

    private void LogToWebsite(string error)
    {
      Uri uri = new Uri(url);
      HttpWebRequest httpWebRequest	= (HttpWebRequest) WebRequest.Create(uri);
      httpWebRequest.Method	= "POST";
      httpWebRequest.ContentType = "application/x-www-form-urlencoded";

      Encoding encoding = Encoding.Default;

      string parameters = string.Format(queryString, HttpUtility.UrlEncode(error));

      // get length of request (may well be a better way to do this)
      MemoryStream memStream = new MemoryStream();
      StreamWriter streamWriter = new StreamWriter(memStream, encoding);
      streamWriter.Write(parameters);
      streamWriter.Flush();
      httpWebRequest.ContentLength = memStream.Length;
      streamWriter.Close();

      Stream stream = httpWebRequest.GetRequestStream();
      streamWriter = new StreamWriter(stream, encoding);
      streamWriter.Write(parameters);
      streamWriter.Close();

      using (HttpWebResponse httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse())
      using (StreamReader streamReader = new StreamReader(httpWebResponse.GetResponseStream()))
      {
        streamReader.ReadToEnd();
      }
    }
  }
}
